Effective Objective C 读书笔记整理
第一章 了解OC
第一条
** | OC | C |
---|---|---|
关系 | C的超集,添加面向对象 | |
语法 | 消息结构,运行的代码依赖于运行环境 函数调用,运行代码 | 编辑器(多态:运行时决定,通过虚函数表) |
性能 | 替换运行时组件 | 重新编译 |
内存 | 所有的对象都分配在堆上,不能分配在栈上 |
关于内存:
与C++不同,OC不允许将OC对象的内存分配到栈(stack)上,只能分配到堆(heap)上。
C++ string str = "123"; 合法
OC NSString str = @"123";非法
OC NSString *str = @"123";合法
所以,OC对象需要指针,ARC也只是针对OC对象(堆上的内存)进行管理,分配在栈上的内存,系统自动清理。
第二条
- 向前声明 @class 的好处:
- 1、是延迟引入,减少类的使用者所需的引入的头文件数量
- 2、解决类之间的相互引用
- #import VS #include : 避免循环引用如果类需要遵从协议,可以在class-continuation分类中)
第三条:多用字面量(语法糖)
- 优势:
- 1、简单、易读、防Nil;特别是NSArray、NSDictionary生成时遇到nil会报错,可以提前排查出问题
- 局限:
- 1、生成不变量,如果需要可变量,需要mutbleCopy;
- 2、也紧紧局限Foudation框架
#define VS 类型变量
- #define 预处理,直接替换,不包含类型变量
- 类型变量:包含类型
- static : 作用域只在当前编译单元(.M)
- const : 不可变
- 在全局变量中,不添加static, 编译器会自动添加外部符号(extern symbol),如果其他文件中也这样定义,会出现重定义,如果需要不同文件中公用变量,需要在.m中进行定义,在.h中添加extern
第五条:枚举
- 1、用枚举表示状态、选项、状态码
- 2、枚举值可能同时使用,定义为2的幂,可位或 组合使用
- 3、NS_ENUM and NS_OPTIONS (宏定义,根据不同模式,选择不同方式) 指明类型,不会采用编译器默认的类型,起到一定保护作用
4、枚举类型 switch 不添加default,这样添加新枚举类型时,会提示错误。只是会未枚举的类型,但是也需要进行Default
第二章 对象、消息、运行时
第六条:
- 实例变量:_someProperty,可通过偏移量访问数据
属性:实例变量+存取方法+属性特质:通过点语法可以读取设置属性;
- 属性会自动生成_property的实例变量,可以通过@synthesize 设置实例变量别名,@dymanic则不自动生成实例变量+存取方法,编译器不会报错,会在运行时找到相关的变量和方法(NSManagerObject)
属性可以分为4类:原子性、读写权限、内存管理语义、方法名
其中需要特别指出的是copy
在定义NSString、NSArray、NSDictionary等支持copy协议,且存在可变类型(比如NSMutableString),属性需要设置为copy,防止一个NSMutableString赋值给属性时,属性就为可修改的
同样NSMutableArray,在设置属性时,需要设置为strong,考虑copy的语义在初始化时:1、copy的属性,建议在初始化时就copy形参 2、在init方法中不建议用存取方法
- (instancetype)initWithname:(NSString *)name { self = [super init]; if (self) { _name = [name copy] } return self; }
ios开发atomic不能保证真正的原子,如果需要锁,需要更深层的锁定机制,出于性能一般都用nonatomatic.MAC OS X 则无此瓶颈.
第7条: 直接访问变量 VS 存取方法
- 直接访问
- 1、直接访问速度更快,无需方法派发
- 2、直接访问,不会调用设置方法,copy属性,不会拷贝属性,而且保留新值释放旧值
- 3、不能KVO
- 4、不便于调试
- 存取方法:
- 1、init中不要用存取方法,防止子类覆盖
- 2、惰性初始化一定要用存取方法
第8条
== 判断指针是否相等, isEqualTo判断类型、属性和hash值 (isEqual会根据类,进行方法分发,工厂方法),在复写isEqual方法时,需要注意其他情况调用super
关于hash值:如果collection类型的属性,直接写死固定值,会造成该固定值的对应的value变多,而影响性能。如果通过整体求hash,也出现中间变量,存在性能损耗。可以多每个属性求hash,在进行与或处理
注意:把对象放入collection之后,改变其内容会造成很严重的后果
{1, 2} -> NSSet *set // {{1, 2}}
{1} -> NSMutableArray *array
array - > set // {{1}, {1, 2}}
{2} -> array
set //{{1,2}, {1, 2}}
set -> NSSet NewSet //{{1, 2}}
第9条 类族
Cocoa里面很多类族实现,这种工厂方式的实现,因此不能用[subA class] == [A class]的方式进行判断,应该使用类型查询方式isKindOfClass进行类型判断。
第10条 关联对象
objc_setAssociateObject objc_getAssociateObject objc_removeAssociateObject
与Dictionary比较 设置关联对象的key一般是“不透明指针”,所以用静态全局变量作为key;同时要指定内存管理语义,用于模仿拥有 和 非拥有关系
第11条 objc_msgSend
消息由接受者、选择子、参数组成,给对象发送消息,相当于对象调用方法
每个类都有一张函数调用表,key为选择子,value为实际调用的函数值。尾调用优化技术,使跳转更加简单:直接跳转,不需要调用堆栈,进行优化。
第12条 消息转发
对象将无法解读的选择子交给其他对象处理,可以模拟多重集成的
第13条 method swizzing
第14条 理解类对象
继承是通过super_class, 元类是通过isa
类对象是单例,可以用==,来判断内存是否相等
第三章 接口和API设计
第15条:用前缀避免命名空间冲突
第16条:
提供“全能初始化方法”:designated initializer OR Initializer from NSCoding;子类与超类不同,子类需要覆盖,超类需要在方法中写Assert
第17条 实现description && debugDescription
第18条 尽量使用不可变对象
1、.h 中readonly, .m中readwrite,
但是在对象外面还可以通过KVC的方式(setValue forKey)进行更改(hack);更加brutal 是通过类型查询信息找到对应实例变量在内存中的偏移量,从而进行设置
- 2、可变的collection,应该通过相关方法,修改可变对象
第19条 使用清晰而协调的命名方式
第20条
为私有方法添加前缀,但是不要单用一个下划线,这是预留苹果公司用的
第21条 理解OC的错误类型
ARC不是异常安全的,抛出异常,未释放的对象不能自动释放。如果想“异常安全”,增加-fobc-arc-exception标志;
严重错误,抛出NSException;不严重用nil,0、NSError
第22条 理解NSCoping
—copyWithZone:(NSZone *)zone
复制对象一般进行浅拷贝,深拷贝可单独写一个方法。深拷贝会将底层数据一起拷贝,包括实例变量。
第四章 协议与分类
分类是建立在OC运行时基础上的;协议一般用于委托模式
第23条 通过委托与数据源进行对象间的通讯
- 1、把需要处理的事件方法定义成协议
- 2、对象从另外一个对象获取数据时,定义成数据源协议
- 3、若有必要,可实现含有位段的结构体,将委托对象是否响应相关协议缓存其中,(直接在setDelegate方法中进行缓存)
第24条 通过分类方法 将类的实现代码分散到便于管理的多个分类中
- 1、划分成不同的功能区
- 2、调试方便,因为分类名会出现在类名后面;
- 3、私有的可以考虑private分类
第25条 为第三方分类及方法添加前缀
如果二个分类提供的方法重名,后编译分类方法会覆盖前面分类方法,分类编译顺序与添加到工程中的顺序有关;如果方法名相同,分类会覆盖苹果自带的方法
第26条 不要在分类中申明属性
不要再分类中声明属性(class-continuation除外),虽然技术上可行。如果声明,会出现warning,原因是分类中无法合成与声明属性相关的变量,所以需要在分类中实现存取方法,并且实现中声明为@dynamic,意思就运行时在提供。当然关联对象也可以实现这种需求,但是仍然建议只在分类中提供方法
第27条 使用class-continuation隐藏实现细节
为什么要有这种分类:因为可以定义方法和实例变量。
隐藏实例方法和方法,也可以避免不必要头文件的引入,特别是对于OC++而言,引入的C++头文件。这样.h中进行向前声明,避免引入不必要的头文件,
也可以将类遵循的协议放在class-continuation,但是向前声明delegate却会有警告,因为引入.h文件,编译器看不到协议的定义及包含的方法。
为什么可以定义方法和实例变量:因为ABI机制,我们无须知道对象大小也可以使用。
第28条 通过协议提供匿名对象
如果具体类型不重要,只是能响应特定方法,那么可使用匿名对象表示,声明为id类型,来隐藏类型名称
第五章 内存管理
第29条
1、对象创建出来引用计数至少为1,因为在alloc 或者init方法中,其他对象对其进行持有。(思考下面autorelease)
2、引用计数的跟对象是NSApplication 或者 UIApplication,都是在main函数中。
3、set方法中MRC的顺序 保留新的值,释放旧的值,在赋值。
-(void)setFoo:(id)foo{
[foo retain];
[_foo release];
_foo = foo;
}
//这种情况下,如果foo 和 _foo是同一个值,就会出现问题
-(void)setFoo:(id)foo{
[_foo release];
_foo = [foo retain];
}
4、autorelease 延长对象生命期,在跨越方式调用便捷后依然存活一段时间。即会在稍后将引用计数减1,通常是下一个event loop,也可能更早(自动释放池会被释放)。这样就方便了函数调用返回对象不会被立即release或者无法release,导致内存泄露。
第30条
ARC只负责OC对象的内管管理,CoreFoundation 对象 不归 ARC 管理,要进行手动CFRetian/CFRelease
ARC在调用下面方法(retain,release,autorelease,dealloc)是非法的,并且它并不是通过OC消息转发机制,而是直接通过底层C语言,性能更好,而且因为保留释放比较频繁,可以对其进行抵消,节省CPU周期。比如objc_autorrelaseReturnValue + retain = objc_RetainautoreleaseReturnValue;这一过程是可通过标志位完成
ARC命名规则:
alloc、new、copy、mutablecopy 生成的对象,要负责释放对象,而其他方法则不需要,会在方法最后添加auotorelease在稍微释放。
变量的内存管理语义:
ARC :_objc = [SomeClass New];
MRC:id tmp = [SomeClass New]; _objc = [tmp retian]; [tmp release];
ARC如何清理实例变量:通过OC++的cleanup routine,调用回收对象的析构函数.cxx_destruct方法,并且自动调用超类的dealloc方法
第31条 在dealloc中只释放非OC对象引用并解除监听
运行时会在适当的时候调用dealloc,不要主动调用dealloc,但是手动 需要最后调用 super dealloc
1、开销较大或者系统内资源(比如文件描述符、套接字、大块内存等)不在dealloc中进行,应该单独提供方法进行释放,还有一个原因是系统并不保证每个创建出来的对象dealloc都会执行,也可以考虑在Appdelegate中种植方法执行清理,防止内存泄露
2、在dealloc不建议调用其他函数,防止调用过程中对象已经销毁。也不要调用属性的存取方法,因为有人会对其进行覆盖,也可能处于KVO下。(这个存取方法待讨论)
3、self.tableView.delegate 设置为nil
特别需要注意的是:
iOS 8 下tableView的 delegate 和 dataSource 是 assign的,如果不设置为nil会发生野指针crash
第32条 编写异常安全的代码,留意内存管理问题
MRC时,可以将内存释放写在finnal里面(提问:为什么不能写在try 和 catch 里面),但是变量就必须放在块的外面
ARC时:不会自动处理try catch的内存管理,因为ARC不能调用release,所以需要很多样板代码,进行跟踪清理对象,影响运行时性能。因为ios认为因为异常而终止程序,内存管理也就没有必要了。
1、通过-fobjc-arc-exceptions进行开启安全异常处理,默认情况是关闭的。OC++模式是默认开启的
2、建议通过NSError方式进行错误捕捉。
第33条 以弱引用避免保留环
垃圾回收(Garbage collector)会检查引用环,并且会将所有的引用对象都回收。但是ios从未支持过该功能
ARC weak引用会自动清理,由运行时系统来实现
第34条
以自动释放块 降低内存峰值。特别是读取大块数据时,比如图像,数据库。
1、自动释放池存放在栈上,对象收到autorealease方法后,系统将其放在最顶端的池里
2、推荐@autoreleasepool,NSAutorelasePool 在MRC时,需要drain来进行释放,而且“比较重”
第35条 僵尸对象调试 内存管理问题
当对象已经释放,但是还没被覆盖时,调用这块内存会正常工作,但是存在很大风险。Cocoa:系统在回收对象时,可以不真的将其回收,而是把它转化为僵尸对象。NSZombieEnabled设置为yes,或者在scheme中勾选。
僵尸类是从NSZombie模板复制出来的,并且可以保留原类名字,不采用继承的方式,是因为效率因素的考虑。
其实现原理是:
修改对象的isa指针,让它指向僵尸类,使对象变成僵尸对象,僵尸类能影响所有的选择子:打印相关消息,并且终止程序。
创建新类,并且转化为僵尸对象:
在消息转发机制中,forwarding相应所有的选择子
第36条 不要用retainCount,ARC后正式废弃
while ([objc retianCount]) {
[objc release]
}
1、objc可能会在后续自动释放,在此释放会crash
2、retainCount 可能永远不为0,系统优化释放行为,为1是就回收了。没有1到0的过程。
第六章 块与大中枢派发
第37条:理解“块”
1、块:只要有支持次特性的编译器以及能执行块的运行期组件,就可以在C、C++、OC、OC++中使用。(
待理解???
)2、块定义,参考
局部变量:
1returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
>
属性:
>
>
方法形参:
>
实参:
1 [someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
>
typedef
>
- 3、关于捕获外部变量:运行块所需的全部信息都可以在编译期确定。而且是捕获栈上的变量,如果要修改栈上的变量,需要声明时,加__block
如下,对象存在堆上,所以不需要加__block
注意:需要注意的是,在block内部中,_someInstance实际是self->_someInstance,因此也会捕获self
- 4、块的内部结构
invoke 是指向函数实现
descriptor指向结构体:copy 、 dispose分别对应引用计数的+1和-1
- 5、栈、堆、全局块
定义block的时候,所占内存都是在栈上的。(只是一般而言,如果定义成实例变量,那么就在堆上了)
VS自己尝试一下输出结果
定义的二个块只在if else 作用域内起作用,通过copy 复制到堆上,就成了带引用计数的对象了。可以在定义范围之外的地方使用。
全局块:不捕捉任何对象,运行时是无状态的。全局块的拷贝是个空操作,因为全局块不会被系统回收,相当于单例。这样的处理只是技术上的优化。
|
|
第38条:为块创建typedef
|
|
优点:
- 1、定义变量一样定义block
- 2、重构时如果给Block多增加参数,那么只需修改相应的块签名(typedef),其他引用block的地方也会自动报错,避免遗漏
第39条 用handler块降低代码分散程度
- 1、使用delegate 会使代码结构过于分散,可以直接只用回调块,使块和相关对象放在一起,避免通过delegate透传数据
- 2、通过handler,增加队列参数,决定放在哪个队列上。
- 3、Error块和Succes块 放在一起,可以处理返回结果中数据异常的情况 VS Error 和 Success分开处理,更加清晰。个人更加意向前者
第40条 用块引用其所属对象避免出现保留环
- 1、设计API时,可以考虑在调用完complete的块之后,将环中的某个对象设置为nil,解除环,避免API调用者没有处理保留环的问题。也可以通过weakify的方法。
- 2、API调用者可以通过weakify的方法,解除保留环的问题;
第41条 多用派发队列,少用同步锁
1、派发队列更加单实现同步语义。
GCD之前,有2种方法:- 1、同步块 @synchrnoized(someObject)一般是对self创建锁,但是其中会涉及与self无关的代码,降低代码效率。
- 2、NSLock对象,通过[_lock lock] [lock unlock]加锁,解锁;NSRecuresiveLock 递归锁
同一个锁的同步块,顺序执行
通过atomic 属性同步,这是通过synchrnoized的方式实现的?
- 2、同步和异步派发结合可以实现加锁机制一样的同步问题,但是却不阻塞异步派发的进程,但是仍然无法正确同步
- 3、使用同步队列及栅栏块可以令同步行为更加高效。
- 4、异步派发,需要拷贝块,因此异步派发不一定会比同步快,需要考虑拷贝块与执行块的时间
第42条 多用GCD,少用performSelector系列方法
- 1、会发生warning,导致内存泄漏,因为编译器不知道调用什么选择子,方法签名、返回值,无法通过ARC对返回值进行管理。
- 2、选择子太过局限,返回类型(void或者id,不能是struct)和参数都有局限。
- 3、如果把任务放在指定线程执行,用GCD和块,毕竟块可以捕获外部变量。
第43条 GCD VS 操作队列(NSOperation)
NSOperation 好处:
- 1、取消操作
- 2、指定依赖关系
- 3、通过KVO监控NSoperation对象的属性
- 4、指定操作的优先级
- 5、可以复用NSOperation对象
NSNotifationCenter 就是使用的操作队列
第44条 使用dispatch group进行任务分组
- 1、dispatch_group_async 包含block,用于回调
- 2、dispatch_group_enter && dispatch_group_leave
dispatch_group_wait (阻塞)使用表示group可以阻塞的时间;
dispatch_group_notify(不阻塞),使用group结束的回调。
dispatch_apply 用于重复执行的次数
第45条 dispatch_once 只执行一次、线程安全
1、之前通过@synchronized(self) 创建单例,比dispatch-once慢二倍
2、需要一个标记,标记声明为static 或者global,目的是标记都相同
第46条 不要使用dispatch_get_current_queue
1、dispatch_get_current_queue 已经废弃,只做调试用。最主要是也不准确,如下:
2、派发队列是按照层次组织的,无法单用某个队列对象来描述当前队列这一概念(如上)
3、dispatch_get_current_queue可以解决不可重入代码引起的思索,一般用“队列特定数据”来解决
|
|
用法具体见说明文档(补充链接)
与NSdictionary不同,更像 关联引用,值也是不透明的void指针,ARC很难进行管理,因此最后一个参数是析构函数
第七章 系统框架
第47条:熟悉系统框架
Foundation OC语言:(CoreFoundation : C语言)
第48条 多用块枚举,少用for循环,
- 1、枚举方式:
1.1、for循环 1.2、NSEnumerator 遍历 1.3、快速遍历、块枚举 - 2、块枚举,支持GCD来并发执行遍历操作1234typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {NSEnumerationConcurrent = (1UL << 0),NSEnumerationReverse = (1UL << 1),};
如果提前知道collection对象类型,应修改块签名,指出对象具体的类型